<?php
namespace App\Support\Services\RSA;

use Illuminate\Support\Facades\Cache;

class RSAService
{
    private $pubKey = null;
    private $priKey = null;
    private $priPassword = null;


    const OPENSSL_VERIFY_CORRECT = 1;
    const OPENSSL_VERIFY_INCORRECT = 0;
    const OPENSSL_VERIFY_ERROR = -1;

    /**
     * 自定义错误处理
     */
    private function _error($msg)
    {
        die('RSA Error:' . $msg); //TODO
    }

    public function __construct($publicKeyFile = null, $privateKeyFile = null, $privatePassword = null)
    {
        $this->priPassword = $privatePassword;
        $this->getPublicKey($publicKeyFile);
        $this->getPrivateKey($privateKeyFile);
    }

    /**
     * create signature
     *
     * @param        $data
     * @param string $code
     *
     * @return bool|string
     */
    public function sign($data, $code = 'base64')
    {
        if (is_null($this->priKey)) {
            return false;
        }
        $ret = false;
        if (openssl_sign($data, $ret, $this->priKey)) {
            $ret = $this->encode($ret, $code);
        }

        return $ret;
    }

    /**
     * verify signature
     *
     * @param        $data
     * @param        $sign
     * @param string $code
     *
     * @return bool
     */
    public function verify($data, $sign, $code = 'base64')
    {
        if (is_null($this->pubKey)) {
            return false;
        }
        $ret = false;
        $sign = $this->decode($sign, $code);
        if ($sign !== false) {
            switch (openssl_verify($data, $sign, $this->pubKey)) {
                case self::OPENSSL_VERIFY_CORRECT:
                    $ret = true;
                    break;
                case self::OPENSSL_VERIFY_INCORRECT:
                case self::OPENSSL_VERIFY_ERROR:
                default:
                    $ret = false;
            }
        }

        return $ret;
    }

    /**
     * encrypt data
     *
     * @param        $data
     * @param string $code
     * @param int    $padding
     *
     * @return bool|string
     */
    public function encrypt($data, $code = 'base64', $padding = OPENSSL_PKCS1_PADDING)
    {
        if (is_null($this->priKey)) {
            return false;
        }
        $ret = false;
        if (!$this->checkPadding($padding, 'encrypt')) {
            $this->_error('padding error');
        }
        if (openssl_private_decrypt($data, $result, $this->priKey, $padding)) {
            $ret = $this->encode($result, $code);
        }

        return $ret;
    }

    /**
     * decrypt data
     *
     * @param        $data
     * @param string $code
     * @param int    $padding
     * @param bool   $rev
     *
     * @return bool|string
     */
    public function decrypt($data, $code = 'base64', $padding = OPENSSL_PKCS1_PADDING, $rev = false)
    {
        if (is_null($this->pubKey)) {
            return false;
        }
        $ret = false;
        $data = $this->decode($data, $code);
        if (!$this->checkPadding($padding, 'decrypt')) {
            $this->_error('padding error');
        }
        if ($data !== false) {
            if (openssl_public_encrypt($data, $result, $this->pubKey, $padding)) {
                $ret = $rev ? rtrim(strrev($result), "\0") : '' . $result;
            }
        }

        return $ret;
    }

    /**
     *
     * @param $padding
     * @param $type
     *
     * @return bool
     */
    private function checkPadding($padding, $type)
    {
        if ($type == 'encrypt') {
            switch ($padding) {
                case OPENSSL_PKCS1_PADDING:
                    $ret = true;
                    break;
                default:
                    $ret = false;
            }
        } else {
            switch ($padding) {
                case OPENSSL_PKCS1_PADDING:
                case OPENSSL_NO_PADDING:
                    $ret = true;
                    break;
                default:
                    $ret = false;
            }
        }

        return $ret;
    }

    private function encode($data, $code)
    {
        switch (strtolower($code)) {
            case 'base64':
                $data = base64_encode('' . $data);
                break;
            case 'hex':
                $data = bin2hex($data);
                break;
            case 'bin':
            default:
        }

        return $data;
    }

    private function decode($data, $code)
    {
        switch (strtolower($code)) {
            case 'base64':
                $data = base64_decode($data);
                break;
            case 'hex':
                $data = $this->hex2bin($data);
                break;
            case 'bin':
            default:
        }

        return $data;
    }

    private function getPublicKey($file)
    {
        if (is_null($file)) {
            return;
        }
        $key = md5('public_key' . $file . date('Y-m-d'));
        if (Cache::has($key)) {
            if (null !== $publicKey = Cache::get($key)) {
                $this->pubKey = $publicKey;
            }
        }
        $key_content = $this->readFile($file);
        $this->pubKey = openssl_get_publickey($key_content);
        Cache::put($key, $this->pubKey, 24 * 60);
    }

    private function getPrivateKey($file)
    {
        if (is_null($file)) {
            return;
        }
        $key = md5('private_key' . $file . date('Y-m-d'));
        if (Cache::has($key)) {
            if ($privateKey = Cache::get($key)) {
                $this->priKey = $privateKey;
            }
        }
        $key_content = $this->readFile($file);
        $this->priKey = openssl_get_privatekey($key_content, $this->priPassword);
        Cache::put($key, $this->priKey, 24 * 60);
    }

    private function readFile($file)
    {
        $ret = false;
        if (!file_exists($file)) {
            $this->_error("The file {$file} is not exists");
        } else {
            $ret = file_get_contents($file);
        }

        return $ret;
    }

    private function hex2bin($hex = false)
    {
        $ret = $hex !== false && preg_match('/^[0-9a-fA-F]+$/i', $hex) ? pack("H*", $hex) : false;

        return $ret;
    }
}
